import struct
import random
import string
import binascii

import tkinter as tk
from tkinter import ttk
from tkinter import filedialog
import socket

from usertable import *
from usertable import globalVal
from stringhandle import *
from certificate import *

from gmssl import sm4
#默认6544收

def makesendconnect(targetSK,localIP,cert):#建立发送连接功能，实质内容
	skchat=socket.socket()#建立发送消息的对方socket套接字
	skchat.settimeout(30)#超时时间30s
	try:
		skchat.connect(targetSK)#发送方试图发起连接
		globalVal.receivesocket.append(skchat)#将用于向对方发送数据的套接字记录下来
		#因为向对方发送数据的端口不固定，由操作系统分配
		lport=skchat.recv(10)#这里需要对方反馈告知自己的发送端口号是多少
		lport=int(str(lport.decode()))#将带有端口号的信息解码得知具体内容
		globalVal.sendsocket.append(skchat)#将发送数据用的socket记录下来
	except socket.timeout:
		puterror_win("连接已超时")#对方不说话就报错
		return -1
	except ConnectionRefusedError:
		puterror_win("对方拒绝连接")#这个真没办法
		return -1
	
	
	x=makesendeiveconnection_certverify(skchat,cert)#在线证书验证，如果验签通过则接受通信否则拒绝通信
	if x==False:
		puterror_win("证书验签失败，已阻止一个通信请求")
		return -1

	sessionkey=''
	skchat.send("ask_key_factor".encode())#向接收方询问：接收方密钥因子与总密钥的和
	keychangemessage=skchat.recv(14)#收到来自接收方的疑问：接收方向发送方询问 发送方密钥因子与总密钥的和
	if keychangemessage==b"ask_key_factor":#确认接收方真的问了这个问题
		keylength=len(globalVal.mainkey)#获取密钥长度
		keyfactor_send=randomstring(keylength)#生成发送方密钥因子
		key_sendaddmain=stringadd_samelength(globalVal.mainkey,keyfactor_send)#发送方密钥因子与总密钥相加
		skchat.send(key_sendaddmain.encode('utf-8'))#将这个和发送给接收方
		keychangemessage=skchat.recv(keylength).decode('utf-8')#获取接收方密钥因子与总密钥的和
		sessionkey=stringadd_samelength(key_sendaddmain,keychangemessage)#生成会话密钥的值
	user=onlineuser(targetSK[0],receiveport_l=targetSK[1])#建立新的连线用户对象
	user.senden=True#设置为可发送数据
	user.sendport_l=lport#记录从本地哪一个端口向其发送数据
	user.receiveport_r=targetSK[1]#记录对方用哪一个端口接收数据
	user.sessionkey=sessionkey#将会话密钥的值记录在这个连线用户对象上
	globalVal.useronline_table.append(user)#在连线用户表加以记载
	return lport#返回对应的本地端口，由lport向外发送数据

def makesendconnect_win(localIPin):#建立发送连接功能，图形界面
	
	def makeconnect_button():#按钮触发实质内容
		targetIP=IP_in.get()#从输入框中获取目标IP和端口号
		targetport=int(port_in.get())
		c=globalVal_cert.localcertificates[certlist.index(certselect.get())]
		lport=makesendconnect((targetIP,targetport),localIPin.get(),c)#连接
		return lport#得到本地的发送端口
		
	certlist=[]
	i=0
	for x in globalVal_cert.localcertificates:
		certlist.append((str)(i)+"  "+x.signerID)
		i+=1
	
	sendconwin=tk.Toplevel()
	sendconwin.geometry("300x150")
	sendconwin.title("建立发送连接")
	prompt1=tk.Label(sendconwin,text="请输入对方的IP地址（或主机名）和端口号")
	promptIP=tk.Label(sendconwin,text="目的IP或主机名")
	promptport=tk.Label(sendconwin,text="目的端口号")
	promptcert=tk.Label(sendconwin,text="本地证书选择")
	prompt2=tk.Label(sendconwin,text="默认接收端口号为6544，发送端口号随机")
	IP_in=tk.Entry(sendconwin,width=20)
	port_in=tk.Entry(sendconwin,width=20,text="6544")
	certselect=ttk.Combobox(sendconwin,values=certlist,width=17,state="readonly")
	promptIP.grid(row=1,column=0)
	promptport.grid(row=2,column=0)
	promptcert.grid(row=3,column=0)
	IP_in.grid(row=1,column=1)
	port_in.grid(row=2,column=1)
	certselect.grid(row=3,column=1)
	prompt2.grid(row=4,column=0,columnspan=3)
	prompt1.grid(row=0,column=0,columnspan=3)
	if port_in.get()=='':
		port_in.insert(0,"6544")#因为默认6544接收，这里自动填写
	
	
	conbutton=tk.Button(sendconwin,text="建立连接",command=lambda:makeconnect_button())
	conbutton.grid(row=5,column=0,columnspan=2)

def sendtext(targetIP,targetport,messagebytes,contentText):#文本消息发送功能
	sessionkey=""#建立会话密钥的变量
	for x in globalVal.useronline_table:#从连线用户表里寻找目标用户
		if x.userIP==targetIP and x.receiveport_r==int(targetport):#找到目标用户
			sessionkey=x.sessionkey#获取对应的会话密钥

	encrypter=sm4.CryptSM4()#启动临时加密机
	encrypter.set_key(sessionkey.encode('utf-8'), sm4.SM4_ENCRYPT)#设置会话密钥的使用
	ciphertextbytes=encrypter.crypt_ecb(messagebytes.encode('utf-8'))#文本消息在使用UTF-8编码后进行加密
	messagehead="[text    ]t"#施加标记[text    ]t表示这是一条文本信息
	for x in globalVal.sendsocket:
		sk=x.getpeername()
		if sk[0]==targetIP and sk[1]==int(targetport):#找到目标用户再发送内容
			x.send(bytes(messagehead.encode('utf-8')))#传输的消息内容用UTF-8编码
			x.send(ciphertextbytes)#加密后传送
	return None

def sendfile(targetIP,targetport,filepath,contentText):#发送文件功能
	thefile=open(filepath,"rb")#打开要传送的文件，为了不失一般性这里采用二进制方式而非文本方式
	filename=os.path.basename(filepath)#获取文件名，不含路径
	for x in globalVal.sendsocket:#从自己可以进行发送消息的套接字对象里寻找目标用户
		sk=x.getpeername()#从套接字对象中获取IP和端口号
		if sk[0]==targetIP and sk[1]==int(targetport):#找到目标用户，且只找一个
			#宣布开始发送
			contentText.insert(tk.END,"发往"+targetIP+":"+str(targetport)+"的文件："+filepath+"开始发送\t",'file')
			contentText.update()

			messagebytes="[file    ]s"#宣布传送的内容是一个文件
			x.send(messagebytes.encode('utf-8'))#报头用utf-8格式编写
			x.send(stringlength_extend_256(filename.encode('utf-8'),0))#文件名最长256字节，这里预留256字节空间
			#所以此处在真实文件名后面会有若干个空格作为填充，接收时要去掉
			filelength=os.stat(filepath).st_size#获取文件大小为filelength，以字节为单位，用十进制表示，高位在前
			remainlength=filelength#用remainlength表示剩下的还没传送的内容多少
			while remainlength>0:#发送文件的剩余部分
				packlength=1024 if remainlength>1024 else remainlength#获取数据部分长度，每次最多只能发送1024字节
				filecontent=thefile.read(packlength)#根据每次的报文数据部分长度，读取文件内容
				messagebytes="[file    ]m"#报头中的m标记表明这是带有文件内容的报文
				x.send(messagebytes.encode('utf-8'))

				if packlength>=1000:#因为数据部分长度用4位十进制表示，这里int向字符串转换要补0
					tpacklength=str(packlength)#4位数直接写
				elif packlength>100:
					tpacklength='0'+str(packlength)#三位数补一个0在最高位，以此类推
				elif packlength>10:
					tpacklength='00'+str(packlength)
				else:
					tpacklength='000'+str(packlength)
				x.send(bytes(tpacklength.encode('utf-8')))#将数据部分长度写入报头
				
				x.send(filecontent)#传送文件内容本身
				remainlength-=1024#剩余长度减少1024字节，如果remainlength能小于等于0则说明传完了
				
				try:
					recvback=x.recv(20)#及时接收接收方的反馈
					if recvback!=b'receive successfully':#如果反馈内容有问题，则在此报错
						puterror_win("文件传送过程出错")
						break
				except socket.timeout:#如果迟迟收不到反馈就推测对方可能下线了，或者网卡了
					useronline_tabledelete(targetIP,targetport,1)#删除这个连线用户
					del globalVal.sendsocket[globalVal.sendsocket.index(x)]#删除这个失效的发送套接字
					puterror_win("与"+targetIP+":"+targetport+"的连接超时，文件传送失败")
				
			messagebytes="[file    ]d"#如果文件内容传送完毕则宣布文件结束
			x.send(messagebytes.encode())#实质上的文件内容发送操作
			break
	thefile.close()#关闭文件
	contentText.insert(tk.END,"传送完毕\n",'file')
	return None

def sendimage(contentText,imagepath,targetIP,targetport):#图片传送功能，传输方式和文件传输一模一样
	theimage=open(imagepath,"rb")
	imagename=os.path.basename(imagepath)#打开图片

	for x in globalVal.sendsocket:
		sk=x.getpeername()
		if sk[0]==targetIP and sk[1]==int(targetport):
			messagebytes="[image   ]s"#宣布传送的内容是一个图片
			x.send(messagebytes.encode('utf-8'))
			x.send(stringlength_extend_256(imagename.encode('utf-8'),0))#图片名称处理

			imagelength=os.stat(imagepath).st_size#获取图片大小为imagelength
			remainlength=imagelength#用remainlength表示剩下的还没传送的内容多少
			while remainlength>0:#发送图片的剩余部分
				packlength=1024 if remainlength>1024 else remainlength
				imagecontent=theimage.read(packlength)
				#messagebytes="[image   ]m"
				x.send(messagebytes.encode('utf-8'))
				print(messagebytes)
				if packlength>=1000:
					tpacklength=str(packlength)
				elif packlength>100:
					tpacklength='0'+str(packlength)
				elif packlength>10:
					tpacklength='00'+str(packlength)
				else:
					tpacklength='000'+str(packlength)
				x.send(bytes(tpacklength.encode('utf-8')))

				x.send(imagecontent)#传送图片内容本身
				remainlength-=1024#剩余长度减少1024字节，如果remainlength能小于等于0则说明传完了
			messagebytes="[image   ]d"#如果文件内容传送完毕则宣布文件结束，图片不需要过于可靠的机制，就没有报错功能
			x.send(messagebytes.encode())
			break
	theimage.close()#关闭文件

	return None

def sendtext_by_button(sendText,contentText,targetIPin,targetportin):
	message=sendText.get('0.0',tk.END)
	targetIP=targetIPin.get()
	targetport=targetportin.get()
	sendtext(targetIP,targetport,message,contentText)
	contentText.insert(tk.END,"发往"+targetIP+":"+str(targetport)+"的消息：",'youself')
	contentText.insert(tk.END,message,'text')

def sendfile_by_button(targetIP,targetport,contentText):
	filepath=filedialog.askopenfilename(title="选择要传输的文件")#通过一个对话框获取要传送的文件路径
	sendfile(targetIP,targetport,filepath,contentText)

def sendimage_by_button(contentText,targetIP,targetport):
	imagepath=filedialog.askopenfilename(title="选择要传输的图片文件",
		filetypes=[("jpg/jpeg",[".jpg",'.jpeg']),("png",".png"),("gif",".gif"),("bmp",".bmp")])
	if imagepath=="":
		return
	
	sendimage(contentText,imagepath,targetIP,targetport)